classdef dotsTheDrawablesManager < dotsAllRemoteObjectManagers
    % @class dotsTheDrawablesManager
    % Coordinate dotsDrawable* objects and dotsTheScreen.
    %
    % @ingroup drawable
    
    properties
        % true or false, whether to clear old graphics after each frame
        % @details
        % Use the setIsAccumulating() method instead of normal property
        % setting, for two reasons:
        %   - allows correct behavior for the server counterpart
        %   - easier to set with an anonymous function or fevalable
        % .
        isAccumulating;
        
        % true or false, whether to draw new frames during runBriefly().
        % @details
        % Users should pass an isAnimating argument to mayDrawNextFrame(),
        % rather than setting isAnimating directly.
        isAnimating;
        
        % string name of managedObjects group to draw as a batch
        % @details
        % Use the activateGroup() method instead of normal property
        % setting, for two reasons:
        %   - allows correct behavior for the server counterpart
        %   - easier to set with an anonymous function or fevalable
        % .
        activeGroup;
        
        % cell array of dotsDrawable objects corresponding to activeGroup
        activeDrawables;
        
        % the dotsTheScreen singleton instance
        theScreen;
        
        % topsGroupedList with information about dropped graphics frames.
        % @details
        % droppedFrameList stores the Psychtoolbox Screen "Flip" time for
        % graphics frames that took longer than the expected frame time.
        % The list groups come from activeGroup and mnemonics are clockTime
        % timestamps from just before frame drawing began.
        droppedFrameList;
        
        % Psychtoolbox Screen "Flip" time from the last graphics frame.
        lastFrameTime;
        
        % struct of Psychtoolbox Screen text properties.
        % @details
        % Use setScreenTextSetting() to modify textSettings and coordinate
        % with dotsTheScreeen and any server counterpart.
        textSettings;
        
        % Psychtoolbox Screen index of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowNumber;
        
        % pixel dimensions of the current drawing window [x y x2 y2]
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowRect;
        
        % Hz frame rate of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowFrameRate;
        
        % degrees-to-pixels factor for the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        pixelsPerDegree;
        
        % a foreground color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        foregroundColor;
        
        % a background color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        backgroundColor;
    end
    
    properties (Hidden)
        % value returned from a getScreenProperty transacton
        screenProperty;
    end
    
    methods (Access=private)
        % Constructor is private.
        % @details
        % Use dotsTheDrawablesManager.theObject to access the current
        % instance.
        function self = dotsTheDrawablesManager
            dotsTheMachineConfiguration.applySettingsToObject(self, true);
            self.initialize;
        end
    end
    
    methods (Static=true)
        % Access the current instance.
        function obj = theObject
            persistent self
            if isempty(self) || ~isvalid(self)
                constructor = str2func(mfilename);
                self = feval(constructor);
            end
            obj = self;
        end
        
        % Restore the current instance to a fresh state.
        function reset(varargin)
            factory = str2func([mfilename, '.theObject']);
            self = feval(factory);
            self.set(varargin{:});
            self.initialize;
        end
        
        % Launch a graphical interface to view managed dotsDrawable
        % objects.
        function g = gui
            self = dotsTheDrawablesManager.theObject;
            g = self.managedObjects.gui;
        end
    end
    
    methods
        % Restore the manager singleton to a fresh state.
        function initialize(self)
            self.isAnimating = false;
            
            self.initialize@dotsAllRemoteObjectManagers;
            
            self.mayRequestTransaction = false;
            
            self.initializeLists({'droppedFrameList'});
            
            % get shared property values from the screen
            self.theScreen = dotsTheScreen.theObject;
            dotsTheSwitchboard.shareManyPropertiesOfObject( ...
                self.theScreen.sharedProperties, self);
            self.textSettings = self.theScreen.textSettings;
            
            % get more shared property values
            dotsTheSwitchboard.sharePropertyOfObject( ...
                'foregroundColor', self);
            dotsTheSwitchboard.sharePropertyOfObject( ...
                'backgroundColor', self);
            
            self.setIsAccumulating(false);
            self.lastFrameTime = [];
            self.activateGroup('none');
            
            self.mayRequestTransaction = true;
        end
        
        % Carry out one transaction and draw one frame, as needed.
        % @details
        % Redefines the runBriefly() method of dotsAllRemoteObjectManagers
        % to also draw a frame of animation, if isAnimating is true.
        function runBriefly(self)
            if self.isAnimating && (self.serverMode || ~self.clientMode)
                self.drawNextFrame;
            end
            self.runBriefly@dotsAllRemoteObjectManagers;
        end
        
        function setIsAccumulating(self, isAccumulating)
            self.isAccumulating = isAccumulating;
            if self.clientMode && self.mayRequestTransaction;
                self.setObjectProperty( ...
                    self, 'isAccumulating', isAccumulating);
            end
        end
        
        % Prepare to draw a group of drawables as a batch.
        % @param group string name of a managedObjects group
        % @details
        % Sets activeGroup for the client and server managers.  The objects
        % in the named @a group will be copied to activeDrawables.  If
        % windowNumber is a valid Screen() drawing window, each object in
        % @a group will have its prepareToDrawInWindow() invoked.
        % @details
        % Subsequent calls to mayDrawNextFrame() will invoke mayDrawNow()
        % for each @a group object.
        function activateGroup(self, group)
            self.activeGroup = group;
            if self.clientMode && self.mayRequestTransaction;
                self.setObjectProperty( ...
                    self, 'activeGroup', group);
            end
        end
        
        % Account for an activated group.
        % @details
        % See activateGroup().
        function set.activeGroup(self, activeGroup)
            self.activeGroup = activeGroup;
            if self.managedObjects.containsGroup(activeGroup)
                self.activeDrawables = self.getGroup(activeGroup);
                self.prepareActiveGroupForWindow;
            else
                self.activeDrawables = {};
            end
        end
        
        % Open a Psychtoolbox Screen drawing window with dotsTheScreen.
        function openScreenWindow(self)
            if self.clientMode && self.mayRequestTransaction;
                % request a remote window and get info (synchronously)
                self.requestTransaction( ...
                    [], [], false, 'openScreenWindow', {});
                self.updateScreenSharedProperties;
                
            else
                % open a real window
                self.theScreen.open;
                self.prepareActiveGroupForWindow;
            end
        end
        
        % Close a Psychtoolbox Screen drawing window with dotsTheScreen.
        function closeScreenWindow(self)
            self.isAnimating = false;
            
            if self.clientMode && self.mayRequestTransaction
                % close a remote window and get info (synchronously)
                self.requestTransaction( ...
                    [], [], false, 'closeScreenWindow', {});
                self.updateScreenSharedProperties;
                
            else
                % close the window
                self.theScreen.close;
            end
        end
        
        % Set a new value for one of the Psychtoolbox Screen Text*
        % settings.
        % @param setting one of the Psychtoolbox Screen Text* settings
        % strings
        % @param setting a value to set for @a setting
        % @details
        % Sets a new value for one of the Psychtoolbox Screen Text*
        % settings, such as "TextSize".  Coordinates the setting with
        % dotsTheScreen and, if necessary, the server counterpart of
        % dotsTheDrawables manager.
        % @details
        % Returns a struct of all the Psychtoolbox Screen Text* settings
        % and their current values.  The same struct is stored in
        % the textSettings property.
        function textSettings = setScreenTextSetting(self, setting, value)
            % set the all the settings for the local screen
            self.theScreen.textSettings.(setting) = value;
            
            % return any updates from the local screen
            textSettings = self.theScreen.textSettings;
            self.textSettings = textSettings;
            
            % forward the settings to the counterpart manager
            %   get back the counterpart's updated settings
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], 'textSettings', true, ...
                    'setScreenTextSetting', {setting, value});
            end
        end
        
        % Invoke prepareToDrawInWindow for objects in activeGroup.
        function prepareActiveGroupForWindow(self)
            if self.theScreen.isWindowOrTexture(self.windowNumber)
                drawList = self.activeDrawables;
                for ii = 1:length(drawList)
                    drawList{ii}.prepareToDrawInWindow;
                end
            end
        end
        
        % Draw a frame for the drawables in activeDrawables.
        % @param isAnimating optional true or false, whether to continue
        % drawing frames during subsequent calls to runBriefly().
        % %details
        % Invokes mayDrawNow() for each of the drawables activeDrawables,
        % then invokes dotsTheScreen.nextFrame() to show the results of
        % drawing.  If the time it takes to call mayDrawNow() and
        % nextFrame() exceeds dotsTheScreen.frameSize, assumes that at
        % least one graphics frame was dropped and adds an entry to
        % droppedFrameList.
        % @details
        % Returns the graphics onset estimate as returned from nextFrame().
        function frameTime = mayDrawNextFrame(self, isAnimating)
            if nargin > 1
                self.isAnimating = isAnimating;
                args = {isAnimating};
            else
                args = {};
            end
            
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], 'lastFrameTime', true, ...
                    'mayDrawNextFrame', args);
                
            else
                frameTime = self.drawNextFrame;
            end
        end
        
        % Get a property of dotsTheScreen, locally or remotely.
        % @param property one of the property names of dotsTheScreen
        % @details
        % Accesses the value of one of the properties of dotsTheScreen.  If
        % necessary, accesses the dotsTheScreen used by the server
        % counterpart of dotsTheDrawablesManager.
        % @details
        % Returns the value of @a property for dotsTheScreen.
        function value = getScreenProperty(self, property)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], 'screenProperty', true, ...
                    'getScreenProperty', {property});
                value = self.screenProperty;
            else
                value = self.theScreen.(property);
            end
        end

        % Set a property of dotsTheScreen, locally or remotely.
        % @param property one of the property names of dotsTheScreen
        % @param value the value to set to @a property
        % @details
        % Modifies the value of one of the properties of dotsTheScreen.  If
        % necessary, modifies the dotsTheScreen used by the server
        % counterpart of dotsTheDrawablesManager.
        % @details
        % Returns the value of @a property for dotsTheScreen that was set.
        function value = setScreenProperty(self, property, value)
            if self.clientMode && self.mayRequestTransaction
                self.requestTransaction([], [], false, ...
                    'setScreenProperty', {property, value});
                value = self.screenProperty;
            else
                self.theScreen.(property) = value;
            end
        end
    end
    
    methods (Access = protected)
        % Draw a frame of graphics (used internally).
        function frameTime = drawNextFrame(self)
            begin = feval(self.clockFunction);
            drawList = self.activeDrawables;
            for ii = 1:length(drawList)
                drawList{ii}.mayDrawNow;
            end
            
            frameTime = self.theScreen.nextFrame(self.isAccumulating);
            self.lastFrameTime = frameTime;
            if ~isempty(frameTime) ...
                    && (frameTime-begin) > self.theScreen.frameSize
                self.droppedFrameList.addItemToGroupWithMnemonic( ...
                    frameTime, self.activeGroup, begin);
            end
        end
        
        % Update shared Screen properties (used internally).
        function updateScreenSharedProperties(self)
            shared = self.theScreen.sharedProperties;
            for ii = 1:length(shared)
                self.requestTransaction([], shared{ii}, true, ...
                    'getScreenProperty', shared(ii));
                dotsTheSwitchboard.setSharedPropertyValue( ...
                    shared{ii}, self.(shared{ii}));
            end
        end
    end
end